[Android]DataBindingで双方向の通知を実装してみる
Databindingはサポートライブラリで提供されているバインディングライブラリです XML上でviewとデータを関連づけたり、データ変更に伴うUI動作などのプロセスを自動化させることができ、単純にfindviewIdやButterKnifeの一部の機能の替わりとしても利用が可能です 他にも様々な便利機能が用意されてますが、導入からモデル-view間の通知までのサンプルを紹介してみます
導入
DataBindingの使用にはgradleプラグイン1.5以上が必要になります
gladle
android { .... dataBinding { enabled = true } }
もしくは
project/build.gradle dependencies { .... classpath "com.android.databinding:dataBinder:1.0-rc4" }
app/build.gradle apply plugin: 'com.android.databinding'
基本実装
モデルクラスを準備
public class Login { private String email; private String password; public Login(String email, String password) { this.email = email; this.password = password; } /* getter and setter */ }
XMLに記述
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="login" type="jp.sample.model.Login"/> </data> <LinearLayout <EditText android:id="@+id/email" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="textEmailAddress" android:singleLine="true" android:text="@{login.email}"/> ... </LinearLayout> </layout>
- ルートを< layout >にする
- < data >は< layout >内での変数のラッパーとしての役割
- @{ }構文でプロパティが設定される
- クラスがnullの場合、パラメータにnullや0が設定される
データをバインド
MainActivity @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main); binding.setLogin(new Login("email","password")); }
- レイアウトファイル名に基づいてBindingクラスが生成される
- set***メソッドの名前は< variable >のnameフィールドに依存する
ここまでがDataBindingを利用する上での基本的な部分になります
モデルの変更をviewに反映させる
ObservableFieldを使う
private static class Login { public final ObservableField email = new ObservableField<>(); public final ObservableField password = new ObservableField<>(); }
Login login = new Login() login.email.set("hoge")
<EditText android:id="@+id/email" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="textEmailAddress" android:singleLine="true" android:Text="@{login.email}"/> /* ObservableField */ ...
- モデルの値が変わるとUI(text等)も変わる
- フィールドの型がObservableFieldになってしまう
- プリミティブ型用にはObservableBooleanとかが用意されてる
BaseObservableを継承してつかう
public class Login extends BaseObservable { private String email; private String password; public Login(String email, String password) { this.email = email; this.password = password; } @Bindable public String getEmail() { return email; } @Bindable public String getPassword() { return password; } public void setEmail(String email) { this.email = email; notifyPropertyChanged(BR.email); } public void setPassword(String password) { this.password = password; notifyPropertyChanged(BR.password); } }
Login login = new Login() login.setEmail("hoge")
<EditText android:id="@+id/email" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="textEmailAddress" android:singleLine="true" android:Text="@{login.email}"/> ...
- 通知したいパラメータに@Bindableをつける
- @BindableがついてるプロパティがBRクラスのフィールドに追加される
- notifyPropertyChanged(int fieldId)でviewへの通知を行う
- extendsを消費してしまう(したくない場合はObservableを実装する)
以上、上記のObservableFieldを利用したパターンとBaseObsaervableを継承したパターンがモデルの変更をviewに通知する例になります
モデルとViewの双方向で通知する
public class LoginViewModel extends BaseObservable { private String email; private String password; private boolean isEditMode = false; public LoginViewModel(String email, String password) { this.email = email; this.password = password; } public SimpleTextWatcher emailWatcher = new SimpleTextWatcher() { @Override public void onTextChanged(String value) { isEditMode = true; setEmail(value); isEditMode = false; } }; public SimpleTextWatcher passwordWatcher = new SimpleTextWatcher() { @Override public void onTextChanged(String value) { isEditMode = true; setPassword(value); isEditMode = false; } }; @Bindable public String getEmail() { return email; } @Bindable public String getPassword() { return password; } public void setEmail(String email) { this.email = email; if (!isEditMode) { notifyPropertyChanged(BR.email); } } public void setPassword(String password) { this.password = password; if (!isEditMode) { notifyPropertyChanged(BR.password); } } }
- 双方向で通知がループしないようにフラグを設定
- サンプルではTextWatcherのカスタムクラスを作成してます
public abstract class SimpleTextWatcher implements TextWatcher { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { onTextChanged(s.toString()); } public abstract void onTextChanged(String value); }
カスタムしたTextWatcherをEditTextにバインドするためにaddTextChangeListenerを設定する
<EditText android:id="@+id/email" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="textEmailAddress" android:singleLine="true" android:text="@{viewModel.email}" app:addTextChangedListener="@{viewModel.emailWatcher}"/> ...
これで画面上でEditTextに入力した値をモデルに適用させる事ができました
- DataBindingの機能によって、標準でviewコンポーネントごとにいくつかのリスナー等がxmlで設定できるように用意されます
- 上記はBaseObservableを継承した例ですが、ObservableFieldを利用したパターンでもviewからの通知は可能です。その場合はObservableのプロパティが変更された時のコールバックメソッドがあるので、それを使うと良いでしょう
まとめ
今まではモデルの変更ロジックに応じて、UIの動作を記述していましたがDataBindingの利用でActivityやFragmentがよりスッキリさせる事できそうですね